#!/usr/bin/env python
# -*- coding: utf-8 -*-
#

import  os
import  bsddb

if 'posix' == os.name :
    import  fcntl

from zxstore import KeyDupError, KeyNotFoundError

def debug(format, *args) : print str(format) % args

class   BdbCell(object) :
    '''
    store cell implemented based on Berkeley-DB
    '''
    def __init__(self, store_path) :
        self.__store_path = store_path
        self.__db_filename   = self.__store_path + os.sep + 'storem.db'
        self.__data_filename = self.__store_path + os.sep + 'storem.data'
        self.__delete_log    = self.__store_path + os.sep + 'delete.log'
        self.__access_log    = self.__store_path + os.sep + 'access.log'

        self.__dbh = None
        self.__fh_data = None
        self.__fh_delete_log = None
        self.__fh_access_log = None

        self.__page_size = 8192
        self.__cache_size = 1048576
        # __init__()

    def __del__(self) :
        self.__exit()
        # __del__()

    def __exit(self) :
        if self.__dbh is not None :
            self.__dbh.sync()
            self.__dbh.close()
            self.__dbh = None

        if self.__fh_data is not None :
            self.__fh_data.close()
            self.__fh_data = None

        if self.__fh_delete_log is not None :
            self.__fh_delete_log.close()
            self.__fh_delete_log = None

        # __exit()


    def __init(self) :
        if self.__dbh is None :
            self.__dbh = bsddb.btopen( self.__db_filename, pgsize=self.__page_size, cachesize=self.__cache_size )

        if self.__fh_data is None :
            self.__fh_data = open ( self.__data_filename, "a+b" )

        if self.__fh_delete_log is None :
            self.__fh_delete_log = open ( self.__delete_log, "a+b" )

        # __init()

    ####################################################################

    def insert(self, key, value) :
        '''
        INSERT or IGNORE
        put (store) a key-value pair in store-cell
        '''
        if self.__dbh is None :
            self.__init()

        location = self.__getLocationByKey( key )
        if location is not None :
            raise KeyDupError, key

        line = "%s\t%s\n" % ( key, value )
        length = len( line )
        self.__fh_data.seek(0, os.SEEK_END)
        offset = self.__fh_data.tell()
        self.__fh_data.write( line )
        fid = 0

        location = '%d,%d,%d' % (fid, offset, length)
        self.__dbh[ key ] = location

        #debug('INSERT key=%s location=%s', key, location)
        return  self
        # insert()

    def erase(self, key) :
        '''
        erase a key-value pair by the key
        '''
        if self.__dbh is None :
            self.__init()

        location = self.__getLocationByKey(key)
        if location is None :
            # NOT FOUND
            raise KeyNotFoundError, key

        self.__dbh.isolation_level = None

        try :
            del self.__dbh[ key ]
        except :
            raise

        fid, offset, length = location
        self.__log_deletion(key, fid, offset, length)

        self.__dbh.isolation_level = ''
        return  self

    def update(self, key, value) :
        '''
        UPDATE or IGNORE
        '''
        self.erase(key)
        self.insert(key, value)

        return  self
        # update()

    def replace(self, key, value) :
        '''
        UPDATE or INSERT
        '''
        location = self.__getLocationByKey(key)
        if location is None :
            self.insert(key, value)
        else :
            self.update(key, value)

        return  self
        # replace()


    def find(self, key) :
        '''
        find (query) value by the key in the cell

        return data on success, None on failure
        '''
        if self.__dbh is None :
            self.__init()

        location = self.__getLocationByKey( key )
        if location is None :
            return  None

        fid, offset, length = location
        self.__fh_data.seek(offset, os.SEEK_SET)
        line = self.__fh_data.readline()
        k, v = line.split('\t', 2)

        return  v.strip()
        # find()

    #####################################################################

    def inserts(self, key_values) :
        if  self.__dbh is None :
            self.__init()

        key_values.sort()
        to_ignore = []
        to_insert = []
        for key, value in key_values :
            location = self.__getLocationByKey( key )
            if location is None :
                to_insert.append( (key, value) )
            else :
                to_ignore.append( (key, value) )

        self.__lock()

        # self.__dbh.isolation_level = 'IMMEDIATE'
        # this can improve performance of insertion ??
        # self.__dbh.isolation_level = ''

        fid = 0
        self.__fh_data.seek(0, os.SEEK_END)
        offset = self.__fh_data.tell()
        records = []
        lines = []
        for key, value in to_insert :
            line = "%s\t%s\n" % (key, value)
            length = len( line )
            lines.append( line )
            records.append( (key, fid, offset, length) )
            offset += length

        self.__fh_data.write( ''.join(lines) )
        self.__fh_data.flush()
        del lines

        for key, fid, offset, length in records :
            location = "%d,%d,%d" % (fid, offset, length)
            self.__dbh[ key ] = location

        self.__unlock()

        # TODO : if insert failed, what to do with these data ???

        return  self
        # inserts()

    def updates(self, key_values) :
        if  self.__dbh is None :
            self.__init()

        key_values.sort()
        to_ignore = []
        to_insert = []
        to_delete = []
        for key, value in key_values :
            location = self.__getLocationByKey( key )
            if location is None :
                to_ignore.append( (key, value) )
            else :
                to_insert.append( (key, value) )
                to_delete.append( (key,) + tuple(location) )

        self.__lock()

        # self.__dbh.isolation_level = 'IMMEDIATE'
        # this can improve performance of insertion ??
        # self.__dbh.isolation_level = ''

        # to delete old data
        self.__batch_log_deletions( to_delete )

        # to append new data
        fid = 0
        self.__fh_data.seek(0, os.SEEK_END)
        offset = self.__fh_data.tell()
        records = []
        lines = []
        for key, value in to_insert :
            line = "%s\t%s\n" % (key, value)
            length = len( line )
            lines.append( line )
            records.append( (key, fid, offset, length) )
            offset += length

        self.__fh_data.write( ''.join(lines) )
        self.__fh_data.flush()
        del lines

        for key, fid, offset, length in records :
            location = "%d,%d,%d" % (fid, offset, length)
            self.__dbh[ key ] = location

        self.__unlock()

        # TODO : if insert failed, what to do with these data ???

        return  self
        # updates()

    def replaces(self, key_values) :
        if  self.__dbh is None :
            self.__init()

        key_values.sort()
        to_delete = []
        for key, value in key_values :
            location = self.__getLocationByKey( key )
            if location is not None :
                to_delete.append( (key,) + tuple(location) )

        self.__lock()

        # self.__dbh.isolation_level = 'IMMEDIATE'
        # this can improve performance of insertion ??
        # self.__dbh.isolation_level = ''

        # to delete old data
        self.__batch_log_deletions( to_delete )

        # to append new data
        fid = 0
        self.__fh_data.seek(0, os.SEEK_END)
        offset = self.__fh_data.tell()
        records = []
        lines = []
        for key, value in key_values :
            line = "%s\t%s\n" % (key, value)
            length = len( line )
            lines.append( line )
            records.append( (key, fid, offset, length) )
            offset += length

        self.__fh_data.write( ''.join(lines) )
        self.__fh_data.flush()
        del lines

        for key, fid, offset, length in records :
            location = "%d,%d,%d" % (fid, offset, length)
            self.__dbh[ key ] = location

        self.__unlock()

        # TODO : if insert failed, what to do with these data ???

        return  self
        # replaces()

    def erases(self, keys) :
        if  self.__dbh is None :
            self.__init()

        keys.sort()
        to_delete = []
        keys_to_delete = []
        for key in keys :
            location = self.__getLocationByKey( key )
            if location is not None :
                to_delete.append( (key,) + tuple(location) )
                keys_to_delete.append( key )

        self.__lock()

        # self.__dbh.isolation_level = 'IMMEDIATE'
        # this can improve performance of insertion ??
        # self.__dbh.isolation_level = ''

        # to delete old data
        self.__batch_log_deletions( to_delete )

        # to append new data
        for key in keys_to_delete :
            del self.__dbh[ key ]

        self.__unlock()

        # TODO : if insert failed, what to do with these data ???

        return  self
        # erases()

    #####################################################################

    def __getLocationByKey(self, key) :
        '''
        get (retrieve) location of data for the key
        '''
        if self.__dbh is None :
            self.__init()

        try :
            loc = self.__dbh[ key ]
        except :
            return  None

        #for k in loc.split(',') :
        #    location.append( int(k) )
        location = [ int(k) for k in loc.split(',') ]

        #debug('LOCATE key=%s location=%s', key, loc)
        return  location
        # __getLocationByKey()


    def __log_deletion(self, key, fid, offset, length) :
        if self.__fh_delete_log is None :
            self.__init()

        record = "%s\t%d\t%d\t%d\n" % ( key, fid, offset, length )
        self.__fh_delete_log.write ( record )
        self.__fh_delete_log.flush()

        return  self
        # __log_deletion()

    def __batch_log_deletions(self, deletions) :
        if self.__fh_delete_log is None :
            self.__init()

        d_list = []
        for deletion in deletions :
            d_list.append( "%s\t%d\t%d\t%d\n" % deletion )
        self.__fh_delete_log.write( ''.join(d_list) )
        self.__fh_delete_log.flush()
        # __batch_log_deletions()


    def __lock(self) :
        if 'posix' == os.name :
            fcntl.flock( self.__fh_data.fileno(), fcntl.LOCK_EX )

        return  True

    def __unlock(self) :
        if 'posix' == os.name :
            fcntl.flock( self.__fh_data.fileno(), fcntl.LOCK_UN )

        return  True

